using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;

namespace Imperium.X10
{
	/// <summary>
	/// Enables control of the X10 CM17A (Firecracker) device.
	/// </summary>
	public sealed class CM17A : IDisposable
	{		
		#region Declarations

        static Dictionary<string, CM17A> instances = new Dictionary<string, CM17A>();
		string portName = null;
		SerialPort serialPort = null;
		int refCount = 0;
		int waitDuration = 5;
		
		#endregion
		
		#region Constructor and Initialize Methods

		/// <summary>
		/// Creates the CM17A device and opens the specified port for communication.
		/// </summary>
		/// <param name="portName">The name of the port to open, such as COM1.</param>
		private CM17A(string portName)
		{
			// Set the port properties and open it.
			this.portName = portName;
			serialPort = new SerialPort(portName);

			// Open the port and initialize the device
			serialPort.Open();
			Reset();
			Wait();
			Initialize();
			Thread.Sleep(15); // This helps with stability
		}


		/// <summary>
		/// Gets an instance the CM17A device and opens the specified port for communication.
		/// </summary>
		/// <param name="portName">The name of the port to open, such as COM1.</param>
		public static CM17A Instance(string portName)
		{
			// Check arguments
			if (portName == null || portName == "")
				throw new ArgumentNullException("portName", "portName must not be null or empty.");

			// Lock to make thread-safe
			lock(instances)
			{
                CM17A instance = null;

                if (instances.ContainsKey(portName))
                {
                    instance = instances[portName];
                }
                else
                {
					try
					{
						// Create an instance
						instance = new CM17A(portName);
					}
					catch (Exception ex)
					{
						// There was a problem communicating with the device.
						instance = null;
						throw new IOException(String.Format("There was an error creating the CM17A device on {0}.", portName), ex);
					}
					finally
					{
						if (instance != null)
						{
							// Increase the reference counter and add to the hashtable
							Interlocked.Increment(ref instance.refCount);
							instances.Add(portName, instance);
						}
					}
				}

				return instance;
			}
		}


		#endregion

		#region Public Methods

		/// <summary>
		/// Sends a command to the CM17A device for a specific house code and unit code.
		/// </summary>
		/// <param name="houseCode">The house code of the unit to control.</param>
		/// <param name="unitCode">The number (1-16) of the unit to control.</param>
		/// <param name="command">The command to send to the specified unit.</param>
		public void SendCommand(X10HouseCode houseCode, int unitCode, X10Command command)
		{
			// Check device code
			if (unitCode < 1 || unitCode > 16)
				throw new ArgumentOutOfRangeException("unitCode", unitCode, "The unit code must be between 1 - 16.");
		
			// Used to prevent multiple threads from sending commands at the same time.
			object deviceLock = new object();

			lock(deviceLock)
			{
				// Send the command
				SendHeader();
				SendByte(BuildFirstCommandByte(houseCode, unitCode, command));
				SendByte(BuildSecondCommandByte(houseCode, unitCode, command));
				SendFooter();			
			}
		}
		
		#endregion

		#region IDisposable Members

		/// <summary>
		/// Disposes of the CM17A object and closes the serial port.
		/// </summary>
		public void Dispose()
		{
			// If no references to the class exist, delete it and remove it from the hashtable
			if (Interlocked.Decrement(ref refCount) == 0)
			{
				// Remove the instance from the hashtable and then dispose of it
				lock(instances)
				{
					instances.Remove(portName);
				}

				RealDispose();
			}
		}

		private void RealDispose()
		{
			GC.SuppressFinalize(this);

			if (serialPort != null)
			{
				if (serialPort.IsOpen)
					serialPort.Close();
				
				serialPort = null;
			}
		}

		
		#endregion

		#region Helper Methods

		/// <summary>
		/// Builds the first byte of the two-byte command to send to the device.
		/// </summary>
		/// <param name="houseCode">The house code of the unit to control.</param>
		/// <param name="unitCode">The number (1-16) of the unit to control.</param>
		/// <param name="command">The command to send to the specified unit.</param>
		/// <returns>The first byte of the two-byte binary command.</returns>
		private byte BuildFirstCommandByte(X10HouseCode houseCode, int unitCode, X10Command command)
		{
			byte[] houseCodes = new byte[]{ 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0, 0x00, 0x10, 0x20, 0x30 };

			// Get the house code byte, which serves as the basic command
			byte commandByte = houseCodes[(int)houseCode];
            
            // If the unit code is >= 9, change bit 6 to 1
			if (unitCode >= 9)
				commandByte = (byte)(commandByte | 0x04);

			return commandByte;
		}

		
		/// <summary>
		/// Builds the second byte of the two-byte command to send to the device.
		/// </summary>
		/// <param name="houseCode">The house code of the unit to control.</param>
		/// <param name="unitCode">The number (1-16) of the unit to control.</param>
		/// <param name="command">The command to send to the specified unit.</param>
		/// <returns>The second byte of the two-byte binary command.</returns>
		private byte BuildSecondCommandByte(X10HouseCode houseCode, int unitCode, X10Command command)
		{
			byte[] unitCodes = new byte[]{ 0x00, 0x10, 0x08, 0x18, 0x40, 0x50, 0x48, 0x58, 0x00, 0x10, 0x08, 0x18, 0x40, 0x50, 0x48, 0x58 };

			byte commandByte = 0x00;

			switch (command)
			{
				case X10Command.TurnOn:
					// Get the unit code byte for "on"
					commandByte = unitCodes[unitCode - 1];
					break;

				case X10Command.TurnOff:
					// Get the unit code byte for "on" and modify for "off"
					commandByte = unitCodes[unitCode - 1];
					commandByte = (byte)(commandByte | 0x20);
					break;

				case X10Command.Brighten:
					commandByte = 0x88; // Special value
					break;

				case X10Command.Dim:
					commandByte = 0x98; // Special value
					break;

                case X10Command.AllLightsOn:
                    commandByte = 0x90; // Special value
                    break;

                case X10Command.AllLightsOff:
                    commandByte = 0xA0; // Special value
                    break;

                case X10Command.AllUnitsOff:
                    commandByte = 0x80; // Special value
                    break;
			}
			
			return commandByte;
		}


		/// <summary>
		/// Sends a logical bit to the device and then briefly sleeps the thread.
		/// </summary>
		/// <param name="bit">true if 1, false if 0.</param>
		private void SendBit(bool bit)
		{
			Initialize();

			if (bit)
				serialPort.DtrEnable = false; // Sends 1
			else
				serialPort.RtsEnable = false; // Sends 0

			Wait();	
		}


		/// <summary>
		/// Sends a byte of data to the CM17A device, one bit at a time.
		/// </summary>
		/// <param name="data">The binary data to send.</param>
		private void SendByte(byte data)
		{
			byte bitMask = 0x80; // 10000000

			// Sequentially process each bit from left to right by shifting the bit mask 1 each time
			while (bitMask != 0x00)
			{
				SendBit((bitMask & data) == bitMask); // Determine if current bit is 1 or 0 (true false)
				bitMask >>= 1; // Shift the '1' one place to the right
			}
		}

		
		/// <summary>
		/// Sends the required header for all commands to the device.
		/// </summary>
		private void SendHeader()
		{			
			SendByte(0xD5); // 11010101
			SendByte(0xAA); // 10101010
		}

		
		/// <summary>
		/// Sends the required footer for all commands to the device.
		/// </summary>
		private void SendFooter()
		{
			SendByte(0xAD); // 10101101;
		}

		
		/// <summary>
		/// Resets the device by powering it off.
		/// </summary>
		private void Reset()
		{
			serialPort.DtrEnable = false;
			serialPort.RtsEnable = false;
		}

		
		/// <summary>
		/// Initializes the device.
		/// </summary>
		private void Initialize()
		{
			serialPort.DtrEnable = true;
			serialPort.RtsEnable = true;
		}

		
		/// <summary>
		/// Sleeps the thread for time specified in <see cref="CM17A.WaitDuration">WaitDuration</see>.
		/// </summary>
		private void Wait()
		{
			Thread.Sleep(waitDuration);
		}

		#endregion

		#region Properties

		/// <summary>
		/// Communication port where the CM17A device is attached.
		/// </summary>
		public string PortName
		{
			get
			{
				return this.portName;
			}
		}

		
		/// <summary>
		/// The amount of time in milliseconds for the thread to wait between sending commands to the device.
		/// </summary>
		/// <remarks>The default is 5 milliseconds. Depending upon your machine, you may be able to go as low as 1 millisecond.</remarks>
		public int WaitDuration
		{
			get
			{
				return this.waitDuration;
			}
			set
			{
				if (value < 1)
					throw new ArgumentOutOfRangeException("WaitDuration", value, "WaitDuration must be a positive integer.");

				this.waitDuration = value;
			}
		}

		
		#endregion
	}
}
